// Copyright (c), 2006, adopus consulting GmbH Switzerland, all rights reserved.
package com.purej.ui;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import com.purej.cfg.Cfg;

/**
 * An abstract frame to be used to build the main frame of a UI application. It implements a template pattern
 * to build to user-interface components and offers some commonly used functionality, for example loading
 * and saving configurations and starting/handling (background-)tasks.
 *
 * @author Stefan Mueller
 */
public abstract class Application extends JFrame implements TaskListener {
  private static final long serialVersionUID = 1L;
  // A set of keys for default UI preferences handling:
  private static final String KEY_WINDOW_STATE = "windowState";
  private static final String KEY_WINDOW_HEIGHT = "windowHeight";
  private static final String KEY_WINDOW_WIDTH = "windowWidth";
  private static final String KEY_WINDOW_X = "windowX";
  private static final String KEY_WINDOW_Y = "windowY";

  // Basic application settings:
  private final int _defaultWindowWidth;
  private final int _defaultWindowHeight;

  // Config file and object:
  private final File _cfgFile;
  private final Cfg _cfg;

  // The task handling stuff:
  private final List<TaskListener> _taskListeners = new ArrayList<TaskListener>();
  private volatile Task<?> _currentTask;

  /**
   * Creates a new instance of this frame.
   *
   * @param applicationName the application name to be shown as title of the main frame
   * @param iconFile the path to the icon file to be loaded as icon of the main frame
   * @param applicationCfg path to the application config file path, relative to user.home or null if no application config should be used
   * @param defaultWindowWith the default with
   * @param defaultWindowHeight the default height
   */
  public Application(String applicationName, String iconFile, String applicationCfg, int defaultWindowWith, int defaultWindowHeight) {
    _defaultWindowWidth = defaultWindowWith;
    _defaultWindowHeight = defaultWindowHeight;

    // Load the configuration:
    _cfgFile = getCfgFile(applicationCfg);
    _cfg = loadCfg();

    // Set the look and feel:
    configureLookAndFeel();

    // Create the layout - the content pane itself is a GridBagLayout:
    getContentPane().setLayout(new GridBagLayout());
    createUI();

    // Set the title and an icon:
    setTitle(applicationName);
    ImageIcon icon = getIcon(iconFile);
    if (icon != null) {
      setIconImage(icon.getImage());
    }

    // Register the listeners (implemented by subclasses):
    addListeners();

    // Restore the window state as defined in the UI preferences:
    restoreWindowState();

    // Add this application as a task-listener:
    addTaskListener(this);
  }

  // =======================================================================
  // ======================== Cfg Handling Methods =========================
  // =======================================================================

  private static File getCfgFile(String applicationCfg) {
    // Get user-specific config if user.home existing:
    String userHome = System.getProperty("user.home");
    if (userHome != null || applicationCfg == null || applicationCfg.length() == 0) {
      return new File(userHome + "/" + applicationCfg);
    }
    return null;
  }

  private Cfg loadCfg() {
    if (_cfgFile != null && _cfgFile.exists()) {
      try {
        // Load the data from the disk if the file exists:
        return new Cfg(_cfgFile);
      }
      catch (Exception e) {
        showError("An error occurred reading the UI configuration!", "Configuration read error", e);
      }
    }
    return new Cfg();
  }

  private void saveCfg() {
    try {
      if (_cfgFile != null) {
        _cfg.store(_cfgFile);
      }
    }
    catch (Exception e) {
      showError("An error occurred saving the UI configuration!", "Configuration save error", e);
    }
  }

  /**
   * Returns the configuration to be used.
   *
   * @return the configuration
   */
  public final Cfg getCfg() {
    return _cfg;
  }

  // ===================================================================
  // ======================== UI Setup Methods =========================
  // ===================================================================

  /**
   * Creates the user interface layout. Must be overwritten by subclasses.
   */
  protected abstract void createUI();

  /**
   * Adds all the different action listeners to the various UI elements. Must be implemented by subclasses.
   */
  protected abstract void addListeners();

  /**
   * Restores the window state from the UI preferences.
   */
  protected final void restoreWindowState() {
    // Set a default size and window state from the config:
    int state = _cfg.getInt(KEY_WINDOW_STATE, Frame.NORMAL);
    int windowX = _cfg.getInt(KEY_WINDOW_X, 0);
    int windowY = _cfg.getInt(KEY_WINDOW_Y, 0);
    int windowWidt = _cfg.getInt(KEY_WINDOW_WIDTH, _defaultWindowWidth);
    int windowHeight = _cfg.getInt(KEY_WINDOW_HEIGHT, _defaultWindowHeight);

    setSize(new Dimension(windowWidt, windowHeight));
    setExtendedState(state);
    if ((state & Frame.ICONIFIED) == Frame.ICONIFIED) {
      // Iconified state will not be recovered, does not make sense:
      setExtendedState(Frame.NORMAL);
    }
    if (windowX != 0 && windowY != 0) {
      setLocation(new Point(windowX, windowY));
    }
    else {
      // Center on screen. Note: A version for pre-jdk1.4 would be:
      // Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
      // setLocation((screenSize.width - width) / 2, (screenSize.height - height) / 2);
      setLocationRelativeTo(null);
    }
  }

  /**
   * Saves the window state to the UI preferences.
   */
  protected final void saveWindowState() {
    // Save the current windows state:
    int state = getExtendedState();
    _cfg.put(KEY_WINDOW_STATE, state);

    if (state == Frame.NORMAL) {
      // If not minimized / maximized, save the location / size on screen:
      Point location = getLocation();
      _cfg.put(KEY_WINDOW_X, (int) location.getX());
      _cfg.put(KEY_WINDOW_Y, (int) location.getY());

      Dimension size = getSize();
      _cfg.put(KEY_WINDOW_WIDTH, (int) size.getWidth());
      _cfg.put(KEY_WINDOW_HEIGHT, (int) size.getHeight());
    }
  }

  /**
   * Sets the look and fell options to produce nice UIs.
   * Might be overwritten by subclasses for a different handling...
   */
  protected void configureLookAndFeel() {
    try {
      String osName = System.getProperty("os.name");
      if (osName.toUpperCase().indexOf("WINDOWS") >= 0) {
        UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
      }
      else if (osName.startsWith("Mac")) {
        // do nothing, use the Mac Aqua L&f, should be the default...
      }
      else {
        // just use the platform default...
      }
    }
    catch (Exception e) {
      // Should never happen...
      e.printStackTrace();
    }
  }

  /**
   * This method should be used to add the main components (eg. JPanels) to this main frame.
   *
   * @param component the component to add
   * @param gridX the x position
   * @param gridY the y position
   * @param weightX the x resize weight
   * @param weightY the y resize weight
   */
  protected final void addMainComponent(Component component, int gridX, int gridY, int weightX, int weightY) {
    GridBagConstraints gbc = new GridBagConstraints();
    gbc.gridx = gridX;
    gbc.gridy = gridY;
    gbc.weightx = weightX;
    gbc.weighty = weightY;
    gbc.fill = GridBagConstraints.BOTH;
    getContentPane().add(component, gbc);
  }

  // ==================================================================
  // ======================== Utility methods =========================
  // ==================================================================

  /**
   * Closes this applications. After disposing, this method calls System.exit().
   */
  public final void terminate() {
    // Save the current window state:
    saveWindowState();

    // Save the UI preferences:
    saveCfg();
    dispose();
    System.exit(0);
  }

  /**
   * Will be called before this application terminates. This method might be overwritten
   * by subclasses to do some termination work, like saving settings.
   */
  protected void beforeTerminate() {
    // Nothing to do by default...
  }

  /**
   * Shows the specified error message on a {@link JOptionPane}.
   *
   * @param text the message text
   * @param title the message title
   */
  public void showError(String text, String title) {
    showMessage(text, title, JOptionPane.ERROR_MESSAGE);
  }

  /**
   * Shows the specified error message on a {@link JOptionPane}.
   *
   * @param text the message text
   * @param title the message title
   * @param e the exception
   */
  public void showError(String text, String title, Exception e) {
    // TODO: Show a dialog including exception stacktrace...
    //e.printStackTrace();
    showMessage(text, title, JOptionPane.ERROR_MESSAGE);
  }

  /**
   * Shows the specified message on a {@link JOptionPane}.
   *
   * @param text the message text
   * @param title the message title
   * @param type the message type
   */
  public void showMessage(String text, String title, int type) {
    JOptionPane.showMessageDialog(this, text, title, type);
  }

  /**
   * Looks up an icon for the specified filename. Returns the Image object or null if not found.
   *
   * @param iconFile the icon file name
   * @return the ImageIcon if found or null if not existing
   */
  public final ImageIcon getIcon(String iconFile) {
    URL url = getClass().getClassLoader().getResource(iconFile);
    if (url != null) {
      return new ImageIcon(url);
    }
    return null;
  }

  // ========================================================================
  // ======================== Task Handling Methods =========================
  // ========================================================================

  /**
   * Returns the currently running task. Can be only one at a time.
   *
   * @return the currently running task
   */
  public final Task<?> getCurrentTask() {
    return _currentTask;
  }

  /**
   * Starts the given task and registers it as the currently running task.
   * @param task the task to be started
   */
  public final void startTask(Task<?> task) {
    if (_currentTask != null) {
      showError("There can be only one running task at a time!", "Task Error");
    }
    else {
      _currentTask = task;
      task.execute(_taskListeners);
    }
  }

  /**
   * @return the list of registered {@link TaskListener} objects
   */
  public final List<TaskListener> getTaskListeners() {
    return _taskListeners;
  }

  /**
   * @param listener the listener to add
   */
  public final void addTaskListener(TaskListener listener) {
    _taskListeners.add(listener);
  }

  /**
   * @param listener the listener to add
   */
  public final void removeTaskListener(TaskListener listener) {
    _taskListeners.remove(listener);
  }

  @Override
  public void taskStarted(Task<?> task) {
    // Nothing to do by default, might be overwritten...
  }

  @Override
  public void taskFinished(Task<?> task, String info) {
    _currentTask = null;
  }

  @Override
  public void taskStatusMessage(Task<?> task, String message) {
    // Nothing to do by default, might be overwritten...
  }

  @Override
  public void taskErrorMessage(Task<?> task, String message) {
    showError(message, "Task Execution Error");
  }
}
